"""
**Concept of DoubleStack:**
A DoubleStack is a data structure that implements two separate stacks, allowing elements to be inserted and removed from each stack independently. It is essentially two stacks sharing the same underlying storage, where one stack operates from one end and the other stack operates from the opposite end.

**Properties of DoubleStack:**
1. Two Stacks: A DoubleStack consists of two separate stacks that operate independently.
2. Stack Capacity: The DoubleStack has a maximum capacity that determines the total number of elements it can hold.
3. Top Pointers: The DoubleStack maintains two top pointers, one for each stack, to keep track of the topmost elements.
4. Size Tracking: Each stack maintains its own size, indicating the number of elements it currently holds.
5. Fullness: The DoubleStack can be considered full if both stacks reach their maximum capacity.
6. Emptiness: The DoubleStack is considered empty if both stacks are empty.

**Operations of DoubleStack:**
1. Push1: Inserts an element onto the top of the first stack.
2. Push2: Inserts an element onto the top of the second stack.
3. Pop1: Removes and returns the element from the top of the first stack.
4. Pop2: Removes and returns the element from the top of the second stack.
5. Peek1: Returns the element at the top of the first stack without removing it.
6. Peek2: Returns the element at the top of the second stack without removing it.
7. IsEmpty1: Checks if the first stack is empty.
8. IsEmpty2: Checks if the second stack is empty.
9. IsFull: Checks if the DoubleStack is full.
10. Length: Returns the total number of elements in the DoubleStack.

The DoubleStack allows independent operations on both stacks, enabling separate manipulation and access to elements in each stack. It provides flexibility in organizing data and facilitates various applications where the need for multiple stacks arises.

"""

class DoubleStack:
    """
    DoubleStack is a data structure that implements two stacks in a single array.
    
    Attributes:
        stack (list): The underlying data structure.
        top1 (int): The index of the top element of the first stack.
        top2 (int): The index of the top element of the second stack.
        max_size (int): The maximum size of the double stack.
    """

    def __init__(self, max_size):
        """
        Initializes an empty DoubleStack.

        Args:
            max_size (int): The maximum size of the double stack.
        """
        self.stack = [None] * max_size
        self.top1 = -1
        self.top2 = max_size

    def push1(self, item):
        """
        Pushes an element onto the top of the first stack.

        Args:
            item: The element to be pushed onto the first stack.
        """
        if self.top1 < self.top2 - 1:
            self.top1 += 1
            self.stack[self.top1] = item
        else:
            raise Exception("Stack Overflow")

    def push2(self, item):
        """
        Pushes an element onto the top of the second stack.

        Args:
            item: The element to be pushed onto the second stack.
        """
        if self.top1 < self.top2 - 1:
            self.top2 -= 1
            self.stack[self.top2] = item
        else:
            raise Exception("Stack Overflow")

    def pop1(self):
        """
        Removes and returns the element from the top of the first stack.

        Returns:
            The element popped from the top of the first stack.
        """
        if self.top1 >= 0:
            item = self.stack[self.top1]
            self.stack[self.top1] = None
            self.top1 -= 1
            return item
        else:
            raise Exception("Stack Underflow")

    def pop2(self):
        """
        Removes and returns the element from the top of the second stack.

        Returns:
            The element popped from the top of the second stack.
        """
        if self.top2 < len(self.stack):
            item = self.stack[self.top2]
            self.stack[self.top2] = None
            self.top2 += 1
            return item
        else:
            raise Exception("Stack Underflow")

    def peek1(self):
        """
        Returns the element at the top of the first stack without removing it.

        Returns:
            The element at the top of the first stack.
        """
        if self.top1 >= 0:
            return self.stack[self.top1]
        else:
            return None

    def peek2(self):
        """
        Returns the element at the top of the second stack without removing it.

        Returns:
            The element at the top of the second stack.
        """
        if self.top2 < len(self.stack):
            return self.stack[self.top2]
        else:
            return None

    def is_empty1(self):
        """
        Checks if the first stack is empty.

        Returns:
            True if the first stack is empty, False otherwise.
        """
        return self.top1 == -1

    def is_empty2(self):
        """
        Checks if the second stack is empty.

        Returns:
            True if the second stack is empty, False otherwise.
        """
        return self.top2 == len(self.stack)

    def is_full(self):
        """
        Checks if the double stack is full.

        Returns:
            True if the double stack is full, False otherwise.
        """
        return self.top1 == self.top2 - 1

    def __len__(self):
        """
        Returns the number of elements in the double stack.

        Returns:
            The number of elements in the double stack.
        """
        return (self.top1 + 1) + (len(self.stack) - self.top2)

    def __repr__(self):
        """
        Returns a string that represents a valid Python expression to recreate the double stack.

        Returns:
            A string representation of the double stack.
        """
        return f"DoubleStack(max_size={len(self.stack)}, stack={self.stack}, top1={self.top1}, top2={self.top2})"

    def __str__(self):
        """
        Returns a string representation of the double stack.

        Returns:
            A string representation of the double stack.
        """
        stack1_repr = ' -> '.join(str(self.stack[i]) for i in range(self.top1, -1, -1))
        stack2_repr = ' <- '.join(str(self.stack[i]) for i in range(self.top2, len(self.stack)))

        gap_length = self.max_size - self.top1 - self.top2

        gap = ' ' + '-' * gap_length + ' ' if gap_length > 0 else ''

        return f"(Bottom1) {stack1_repr} (Top1) {gap} (Top2) {stack2_repr} (Bottom2)"


if __name__ == "__main__":
    # Create a double stack with a maximum size of 10
    double_stack = DoubleStack(10)

    # Push elements onto the first stack
    for i in range(1, 6):
        double_stack.push1(i)

    # Push elements onto the second stack
    for i in range(6, 11):
        double_stack.push2(i)

    # Print the double stack
    print(f"DoubleStack:\n{double_stack}")  # (Bottom1) 5 -> 4 -> 3 -> 2 -> 1 (Top1)      (Top2) 10 <- 9 <- 8 <- 7 <- 6 (Bottom2)

    # Print the double stack representation
    print(repr(double_stack))  # DoubleStack(max_size=10, stack=[5, 4, 3, 2, 1, 10, 9, 8, 7, 6], top1=4, top2=6)

    # Peek at the top of the first stack
    print(f"Peek at stack 1: {double_stack.peek1()}")  # Peek at stack 1: 5

    # Peek at the top of the second stack
    print(f"Peek at stack 2: {double_stack.peek2()}")  # Peek at stack 2: 6

    # Pop elements from the first stack
    print(f"Pop from stack 1: {double_stack.pop1()}")  # Pop from stack 1: 5

    # Pop elements from the second stack
    print(f"Pop from stack 2: {double_stack.pop2()}")  # Pop from stack 2: 6

    # Print the double stack after popping
    print(f"DoubleStack after pop:\n{double_stack}")  # (Bottom1) 4 -> 3 -> 2 -> 1 (Top1)      (Top2) 10 <- 9 <- 8 <- 7 (Bottom2)

    # Check if the first stack is empty
    print(f"Is stack 1 empty: {double_stack.is_empty1()}")  # Is stack 1 empty: False

    # Check if the second stack is empty
    print(f"Is stack 2 empty: {double_stack.is_empty2()}")  # Is stack 2 empty: False

    # Check if the double stack is full
    print(f"Is double stack full: {double_stack.is_full()}")  # Is double stack full: False

    # Get the number of elements in the double stack using len()
    print(f"Double stack size: {len(double_stack)}")  # Double stack size: 8
